/**
 * Title:       Bootstrap.java
 * Copyright:   Copyright (c) 1996-2007 The Agent Factory Working Group. All rights reserved.
 * Licence:     This file is free software; you can redistribute it and/or modify
 *              it under the terms of the GNU Lesser General Public License as published by
 *              the Free Software Foundation; either version 2.1, or (at your option)
 *              any later version.
 *
 *              This file is distributed in the hope that it will be useful,
 *              but WITHOUT ANY WARRANTY; without even the implied warranty of
 *              MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *              GNU Lesser General Public License for more details.
 *
 *              You should have received a copy of the GNU Lesser General Public License
 *              along with Agent Factory; see the file COPYING.  If not, write to
 *              the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
 *              Boston, MA 02111-1307, USA.
 */
package com.agentfactory;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintStream;

import java.text.SimpleDateFormat;

import java.util.ArrayList;
import java.util.Date;


/**
 * A Bootstrap class to construct the inital classpath and pass on relevant parameters
 *   to the com.agentfactory.platform.InitialisationManager
 *
 * @author David Lillis
 * @version $Id: Bootstrap.java,v 1.6 2007/11/02 17:01:34 ishnid Exp $
 */
public class Bootstrap {
    private static final String SEPARATOR = System.getProperty( "path.separator" );
    private static final String VERSION = "1.0.1";
    private static final int MODE_RUN = 1;
    private static final int MODE_HELP = 2;
    private static final int MODE_VERSION = 3;
    private static int mode = MODE_RUN;
    private static File scriptFile;
    private static File configFile;
    private static File projectHome;
    private static File outputFile;
    private static File afHome;
    private static String customClassPath;
    private static Integer port;
    private static ArrayList javaOptions = new ArrayList(  );
    private static boolean forwardOutput = false;

    // Hide constructor
    private Bootstrap(  ) {}

    /**
     * Main method for initial Agent Factory startup. Parses arguments and passes those relevant to the RTE
     *
     * @param args Command line arguments
     */
    public static void main( String[] args ) {
        System.out.println( "The Agent Factory Runtime Environment\nCopyright (c) 1996-2007 The Agent Factory Working Group" );

        // where is AF_HOME located
        String afHomeString = System.getProperty( "af.home" );

        // no point going any further without AF_HOME, as we won't find the ``lib'' directory
        if( afHomeString == null ) {
            System.err.println( "[Bootstrap] Error: af.home is not set. Cannot build CLASSPATH" );
            System.exit( 1 );
        }

        afHome = new File( afHomeString );

        // get the project's home directory
        getProjectHomeFromArgs( args );

        // now process the arguments properly
        //  (we need to know the project home before we do this)
        processArgs( args );

        // perform the requisite action depending on what processArgs() produced
        switch( mode ) {
        case MODE_HELP:
            printHelp(  );

            break;

        case MODE_VERSION:
            printVersion(  );

            break;

        default:
            run(  );

            break;
        }
    }

    /**
     * Get a list of all the .jar files in a given directory. Files are identified as .jar files
     *   only by looking at their extensions
     *
     * @arg searchDir The directory in which to search for .jar files
     * @return An array of the jar files found in the directory (if any), or @code{null} if
     * @code{searchDir} doesn't represent a directory.
     *
     */
    private static File[] getJarFilesIn( File searchDir ) {
        return searchDir.listFiles( new FilenameFilter(  ) {
                public boolean accept( File dir, String name ) {
                    return name.endsWith( ".jar" );
                }
            } );
    }

    /**
     * Search an array of command line arguments for the project's home directory
     *
     * Project home defaults to the present working directory unless otherwise specified in
     *   the arguments
     *
     * @arg args The arguments originally passed to this Bootstrap program
     * @return The project's home directory
     */
    private static void getProjectHomeFromArgs( String[] args ) {
        // load the default behaviour (i.e. the pwd)
        projectHome = new File( System.getProperty( "user.dir" ) );

        // look for the project directory in the parameters that have been passed to the script
        // stop this search one before the end of the args as the last arg being -ph tells us nothing
        for( int i = 0; i < ( args.length - 1 ); i++ ) {
            if( args[ i ].equals( "-project-home" ) || args[ i ].equals( "-d" ) ) {
                File projectDir = new File( args[ ++i ] );

                if( projectDir.exists(  ) && projectDir.isDirectory(  ) ) {
                    projectHome = projectDir;
                }
                else {
                    System.err.println( "[Bootstrap] Warning: Invalid project directory. Attempting to default to the present working directory" );
                }
            }
        }
    }

    /**
     * Add files or directories to a classpath in a cross-platform manner
     *
     * @arg classpath The StringBuffer that holds the classpath
     * @arg toAdd An array of the files and directories to be added to the classpath
     */
    private static void addTo( StringBuffer classpath, File[] toAdd ) {
        if( toAdd != null ) {
            for( int i = 0; i < toAdd.length; i++ ) {
                classpath.append( ( toAdd[ i ].getAbsolutePath(  ) + SEPARATOR ) );
            }
        }
    }

    /**
     * Print the help text and exit.
     */
    private static void printHelp(  ) {
        System.out.println( "Version " + VERSION + "\n" + "Please see the Agent Factory Manual for Detailed Documentation\n" + "Basic Usage:\n" + "\tagentfactory [-c config_file] [-d project_home] [-h] [-o output] [-p port] [-s script_file] [-v]" );
        System.exit( 0 );
    }

    /**
     * Print version information and exit.
     */
    private static void printVersion(  ) {
        System.out.println( "Version " + VERSION );
    }

    /**
     * Process the command-line arguments and ensure they're valid
     *
     */
    private static void processArgs( String[] args ) {
        for( int i = 0; i < args.length; i++ ) {
            // arguments that require a value to be passed too
            if( i < ( args.length - 1 ) ) {
                // check the port is numeric
                if( args[ i ].equals( "-p" ) || args[ i ].equals( "-p" ) ) {
                    try {
                        port = Integer.valueOf( args[ ++i ] );
                    }
                    catch( NumberFormatException ex ) {
                        System.err.println( "[Bootstrap] Error: specified port is non-numeric" );
                        System.exit( 1 );
                    }
                }

                // script file
                else if( args[ i ].equals( "-s" ) || args[ i ].equals( "-script" ) ) {
                    scriptFile = new File( projectHome, args[ ++i ] );
                }

                // config file
                else if( args[ i ].equals( "-c" ) || args[ i ].equals( "-config" ) ) {
                    configFile = new File( projectHome, args[ ++i ] );
                }

                // output file
                else if( args[ i ].equals( "-o" ) || args[ i ].equals( "-output" ) ) {
                    outputFile = new File( projectHome, args[ ++i ] );
                }

                // project home, but we've extracted this already, so skip it
                else if( args[ i ].equals( "-d" ) || args[ i ].equals( "-project-home" ) ) {
                    i++;
                }

                else if( args[ i ].equals( "-cp" ) || args[ i ].equals( "-classpath" ) ) {
                    customClassPath = args[ ++i ];
                }
            }

            // flag-style parameters (i.e. no values being passed)
            // print help and exit
            if( args[ i ].equals( "-h" ) || args[ i ].equals( "-help" ) ) {
                mode = MODE_HELP;
            }

            // print version and exit
            else if( args[ i ].equals( "-v" ) || args[ i ].equals( "-version" ) ) {
                mode = MODE_VERSION;
            }

            // print output to the screen
            else if( args[ i ].equals( "-f" ) || args[ i ].equals( "-forward" ) ) {
                forwardOutput = true;
            }

            // pass other options to java
            else if( args[ i ].startsWith( "-" ) ) {
                javaOptions.add( args[ i ] );
            }
        }

        // they shouldn't specify a port and a script file
        if( ( port != null ) && ( scriptFile != null ) ) {
            System.err.println( "[Bootstrap] Error: a script file and a port should not both be specified" );
            System.exit( 1 );
        }

        // set a default script file if no file or port were given
        else if( ( port == null ) && ( scriptFile == null ) ) {
            scriptFile = new File( projectHome, "default.aps" );
        }

        // set the default config file if none was given
        if( configFile == null ) {
            configFile = new File( projectHome, "default.cfg" );
        }
    }

    /**
     * Run Agent Factory
     */
    private static void run(  ) {
        // the command to be executed will be constructed in this array
        ArrayList commandBuilder = new ArrayList(  );

        // begin constructing the command to run
        commandBuilder.add( "java" );

        // add any options that should be passed to java
        for( int i = 0; i < javaOptions.size(  ); i++ ) {
            commandBuilder.add( javaOptions.get( i ) );
        }

        commandBuilder.add( "-cp" );

        // make sure the script file and config file (default or otherwise) exist
        if( ( scriptFile != null ) && ! scriptFile.exists(  ) ) {
            System.err.println( "[Bootstrap] Error: script file " + scriptFile.getName(  ) + " does not exist" );
            System.exit( 1 );
        }

        else if( ! configFile.exists(  ) ) {
            System.err.println( "[Bootstrap] Error: config file " + configFile.getName(  ) + " does not exist" );
            System.exit( 1 );
        }

        // set default output file if it's not specified
        if( ( outputFile == null ) && ! forwardOutput ) {
            String scriptFileName = scriptFile.getName(  ).replaceAll( "\\.aps$", "" );
            String configFileName = configFile.getName(  ).replaceAll( "\\.cfg$", "" );
            String date = new SimpleDateFormat( "yyyyMMdd" ).format( new Date(  ) );
            outputFile = new File( projectHome, date + "-" + scriptFileName + "-" + configFileName + ".log" );
        }

        // begin constructing the CLASSPATH
        StringBuffer classpath = new StringBuffer(  );

        // get all jar files in the AF_HOME/lib directory and the PROJECT_HOME/plugins directory
        addTo( classpath, getJarFilesIn( new File( afHome, "lib" ) ) );
        addTo( classpath, getJarFilesIn( new File( projectHome, "plugins" ) ) );

        // add the PROJECT_HOME and PROJECT_HOME/work directory to the CLASSPATH
        // PROJECT_HOME is necessary so that the .aps and .cfg files can be found
        classpath.append( ( new File( projectHome, "work" ) + SEPARATOR + projectHome ) );

        // add the existing environment's classpath
        String systemClassPath = System.getProperty( "java.class.path" );

        // ignore the first part of the system class path, as this is af_rte.jar, which is
        // already in the class path as it's in the AF_HOME/lib directory
        systemClassPath = systemClassPath.substring( systemClassPath.indexOf( SEPARATOR ) + 1 );

        // load the custom classpath if specified by the -cp option
        // this overrides the environment variable, which is the same policy that java itself uses
        if( ( customClassPath != null ) && ( customClassPath.length(  ) > 0 ) ) {
            classpath.append( SEPARATOR + customClassPath );
        }

        else if( ( systemClassPath != null ) && ( systemClassPath.length(  ) > 0 ) ) {
            classpath.append( SEPARATOR + systemClassPath );
        }

        // add the CLASSPATH to the command
        commandBuilder.add( classpath.toString(  ) );
	
        // add af.home and project.home to the command
        commandBuilder.add( "-Daf.home=" + afHome.getAbsolutePath(  ) );
        commandBuilder.add( "-Dproject.home=" + projectHome.getAbsolutePath(  ) );

        // add the class to be executed
        commandBuilder.add( "com.agentfactory.platform.InitialisationManager" );

        // set the output file unless we're to print to the screen
        // if -f annd -o have been specified, this will be handled later on
        if( ! forwardOutput ) {
            commandBuilder.add( "-out" );
            commandBuilder.add( outputFile.getAbsolutePath(  ) );
        }

        // add the script file
        if( scriptFile != null ) {
            commandBuilder.add( "-script" );
            commandBuilder.add( scriptFile.getName(  ) );
        }

        // or add the port
        else if( port != null ) {
            commandBuilder.add( "-port" );
            commandBuilder.add( port.toString(  ) );
        }

        commandBuilder.add( "-config" );
        commandBuilder.add( configFile.getName(  ) );

        String[] command = new String[ commandBuilder.size(  ) ];
        commandBuilder.toArray( command );

        // execute the command
        try {
            Process proc = Runtime.getRuntime(  ).exec( command );

            if( forwardOutput ) {
                // write to this file as well as to the screen if -output is specified
                PrintStream fileOut = null;

                if( outputFile != null ) {
                    fileOut = new PrintStream( new FileOutputStream( outputFile, true ) );
                }

                new StreamHandler( proc.getInputStream(  ), System.out, fileOut ).start(  );
                new StreamHandler( proc.getErrorStream(  ), System.err, fileOut ).start(  );
                proc.waitFor(  );
            }
        }
        catch( IOException e ) {
            System.err.println( "[Bootstrap] Error: Failed to launch the InitialisationManager" );
            e.printStackTrace(  );
        }
        catch( InterruptedException e ) {
            System.err.println( "[Bootstrap] Error: " );
            e.printStackTrace(  );
        }
    }
}

/**
 * Handler for the STDERR and STDOUT from the Agent Factory process to print 
 * output to file/console.
 */
class StreamHandler extends Thread {
    private BufferedReader in;
    private PrintStream fileOut;
    private PrintStream out;

    /**
     * Constructor
     * @param in The stream to read the input from.
     * @param out The stream to write output to.
     * @param fileOut Output stream to write to a file in addition to writing to @code{out}.
     *   If this is @code{null}, then only @code{out} is written to.
     *
     */
    public StreamHandler( InputStream in, PrintStream out, PrintStream fileOut ) {
        this.in = new BufferedReader( new InputStreamReader( in ) );
        this.out = out;
        this.fileOut = fileOut;
    }

    /**
     * Run the handler
     */
    public void run(  ) {
        String line;

        try {
            // if no file output stream was provided, just print to the ``out'' stream
            if( fileOut == null ) {
                while( ( line = in.readLine(  ) ) != null ) {
                    out.println( line );
                }
            }

            // if a file output stream was specified, print to both
            else {
                while( ( line = in.readLine(  ) ) != null ) {
                    out.println( line );

                    // STDOUT and STDERR being printed to the same output stream,
                    //  so synchonisation is necessary
                    synchronized( fileOut ) {
                        fileOut.println( line );
                    }
                }
            }
        }
        catch( IOException e ) {
            System.err.println( "[Bootstrap] Error piping output" );
        }
    }
}